/*****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations
 * under the License.
 ****************************************************************/
package reco.db;

import static com.google.common.base.Preconditions.checkNotNull;

import org.apache.cayenne.BaseContext;
import org.apache.cayenne.CayenneDataObject;
import org.apache.cayenne.DataObjectUtils;
import org.apache.cayenne.DeleteDenyException;
import org.apache.cayenne.PersistenceState;

import reco.db.validation.DomainErrors;
import reco.db.validation.FieldError;
import reco.db.validation.FieldErrors;

/**
 * Domain instances in reco-db, defines a simple interface to do CRUD and to access/modify its {@link Field}.
 * 
 * @param <D>
 *            Class of the described domain.
 */
@SuppressWarnings("serial")
public abstract class Domain<D extends Domain<D>> extends CayenneDataObject {

    /**
     * Unique ID to be used when domains are transient or new (and not persisted).
     */
    public static long NOT_ASSIGNED_ID = 0L;

    private transient DomainErrors errors;

    protected Domain() {
        this(null);
    }

    protected Domain(DomainErrors domainErrors) {
        this.errors = domainErrors;
    }

    /**
     * @returns Gets the domain's unique id.
     */
    public long id() {
        return isNew() ? NOT_ASSIGNED_ID : DataObjectUtils.longPKForObject(this);
    }

    /**
     * Verify if the model is not persisted.
     * 
     * @return <code>true</code> if the object doesn't exist in the DB; <code>false</code> in other way.
     */
    public boolean isNew() {
        return getPersistenceState() == PersistenceState.TRANSIENT || getPersistenceState() == PersistenceState.NEW;
    }

    /**
     * Makes the instance persistent.
     * 
     * @return the instance after it is marked as persistent.
     */
    public boolean save() {
        if (validate().hasErrors()) {
            return false;
        }
        BaseContext.getThreadObjectContext().registerNewObject(this);
        return true;
    }

    public DomainErrors validate() {
        final DomainErrors domainErrors = getErrors();

        for (final Field<?, D> field : getFields()) {
            final Object fieldValue = readProperty(field.getName());
            for (final Constraint<?> fieldConstraint : field.constraints()) {
                if (!fieldConstraint.validateValue(fieldValue)) {
                    final FieldError error = new FieldError(fieldValue, fieldConstraint.getErrorMessage(), fieldConstraint.getErrorArguments());
                    domainErrors.registerErrorOn(field, error);
                }
            }
        }

        return getErrors();
    }

    public abstract Field<?, D>[] getFields();

    /**
     * Marks the instance as deleted.
     * <p>
     * <em>Note:</em> the instance is not immediatly deleted, it only schedules the instance for deletion on the next commit.
     * 
     * @throws DeleteDenyException
     *             if a {@link org.apache.cayenne.map.DeleteRule#DENY} delete rule is applicable for object deletion.
     */
    public void delete() {
        BaseContext.getThreadObjectContext().deleteObject(this);
    }

    /**
     * Gets the actual field value.
     * 
     * @param <T>
     *            Type of the field to retrieve.
     * @param field
     *            {@link Field} to get from the domain.
     * @return the {@link Field} value in the domain.
     * @throws NullPointerException
     *             if <code>field</code> is <code>null</code>.
     */
    public <T> T get(Field<T, ? super D> field) {
        checkNotNull(field, "field");
        @SuppressWarnings("unchecked")
        // we can safely assume fields type
        final T value = (T) readProperty(field.getName());
        return value;
    }

    /**
     * Changes the field value in the domain
     * 
     * @param <T>
     *            Type of the field to change.
     * @param field
     *            {@link Field} to change int the domain.
     * @param newValue
     *            Value to assign to the {@link Field} in the domain.
     * @throws NullPointerException
     *             if <code>field</code> is <code>null</code>.
     */
    public <T> void set(Field<T, ? super D> field, T newValue) {
        checkNotNull(field, "field");
        writeProperty(field.getName(), newValue);
    }

    public DomainErrors getErrors() {
        if (this.errors == null) {
            this.errors = new DomainErrors(this);
        }
        return this.errors;
    }

    public boolean hasErrors() {
        return this.errors != null && this.errors.hasErrors();
    }

    public FieldErrors errorsOn(Field<?, D> field) {
        return getErrors().errorsOf(field);
    }

    public void discardErrors() {
        if (this.errors != null) {
            this.errors.discardErrors();
        }
    }
}
